/**
 * 
 */
package it.unitn.testbed.server.task;

import it.unitn.testbed.client.common.MoteConfig;
import it.unitn.testbed.server.TestbedServiceImpl;
import it.unitn.testbed.server.serial.SerialReceiver;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;

import org.apache.log4j.Logger;

import ch.ethz.ssh2.Connection;
import ch.ethz.ssh2.SCPClient;
import ch.ethz.ssh2.Session;
import ch.ethz.ssh2.StreamGobbler;

/**
 * A thread wrapper for flashing an binary on a mote.
 * 
 * The targets supported by the {@code FlashThread} are given by {@link Type}.
 * The target is given by the {@link MoteConfig#getSource()}.
 * 
 * The flasher must be a thread so that multiple nodes can be flashed in
 * parallel.
 * 
 * @author Stefan Guna
 * @see MoteConfig
 * 
 */
class FlashThread extends Thread {
	/** Targets supported by the flasher. */
	enum Type {
		/**
		 * A local task denotes a mote connected to the local machine of the
		 * application server.
		 */
		LOCAL,
		/**
		 * A network task denotes that the serial forwarder should be used to
		 * flash a mote.
		 */
		NET,
		/**
		 * An SSH task denots that the flasher should use SCP to copy the file
		 * to a remote machine and then invoke remote commands to flash motes
		 * and start/stop serial forwarders.
		 */
		SSH
	}

	public static Logger log = Logger.getLogger(FlashThread.class.getName());

	/**
	 * Converts the string returned by {@link MoteConfig#getSource()} to a
	 * {@link Type}.
	 */
	static Type getSourceType(String source) {
		if (source.contains(":"))
			return Type.NET;
		if (source.contains("@"))
			return Type.SSH;
		return Type.LOCAL;
	}

	/** The file to flash. */
	private File hexFileImage;
	/** The id of the mote */
	private int id;
	private SerialReceiver serialReceiver;
	private TestbedServiceImpl testbedService;
	private Task task;

	/**
	 * Builds a new flasher instance.
	 * 
	 * @param testbedService
	 *            The Tesbed server side implementation to obtain parameters
	 *            such as testbed configuration and so on.
	 * @param hexFileImage
	 *            The .hex image to flash.
	 * @param id
	 *            The mote to be flashed.
	 * @param serialReceiver
	 *            The serial receiver to register the mote.
	 */
	public FlashThread(TestbedServiceImpl testbedService, File hexFileImage,
			int id, SerialReceiver serialReceiver, Task task) {
		this.hexFileImage = hexFileImage;
		this.id = id;
		this.testbedService = testbedService;
		this.serialReceiver = serialReceiver;
		this.task = task;
	}

	/**
	 * Flashes an image on a local host. It first stops the local serial
	 * forwarded using the command in {@code sfStop}, flashes the images on
	 * {@code source} and then starts the serial forwarder with {@code sfStart}.
	 * 
	 * @param source
	 * @param sfStart
	 * @param sfStop
	 */
	private void flashLocal(String source, String sfStart, String sfStop) {
		String cmdFlash[] = new String[] { "tos-bsl", "--telosb", "-c", source,
				"-r", "-e", "-I", "-p",
				hexFileImage.getAbsoluteFile().toString() };

		if (sfStop != null && sfStart != null) {
			String sfStopCmd[] = sfStop.split(" ");
			try {
				execCmd(sfStopCmd);
			} catch (MoteProgramException e) {
				e.setState(MoteProgramException.MoteState.SF_STOP);
				throw e;
			}
		}

		execCmd(cmdFlash);

		if (sfStart != null) {
			String sfStartCmd[] = sfStart.split(" ");
			try {
				execCmd(sfStartCmd);
			} catch (MoteProgramException e) {
				e.setState(MoteProgramException.MoteState.SF_START);
				throw e;
			}
		}
	}

	/**
	 * Flashes an image on a remote host using SSH. It first copies
	 * {@link #hexFileImage} to {@code host}, then it stops the serial forwarder
	 * using the command in {@code sfStop}, it flashes the image on {@code usb}
	 * using {@literal tosbsl} then it starts the serial forwarder with {@code
	 * sfStart}.
	 * 
	 * @param usb
	 * @param host
	 * @param sfStop
	 * @param sfStart
	 */
	private void flashSSH(String usb, String host, String sfStart, String sfStop) {
		try {
			Connection conn = new Connection(host);
			conn.connect();
			if (!conn.authenticateWithPassword(testbedService.getConfig()
					.getSshUser(), testbedService.getConfig().getSshPassword())) {
				log.error("Unable to authenticate!");
				return;
			}

			SCPClient scpClient = conn.createSCPClient();
			scpClient.put(hexFileImage.getAbsolutePath(), "/root");

			if (sfStop != null && sfStart != null)
				remoteExec(conn, sfStop);

			if (remoteExec(conn, "tosbsl " + usb + " /root/"
					+ hexFileImage.getName()) != 0)
				log.error("Unable to program " + usb + "@" + host);

			remoteExec(conn, "rmwrap /root/" + hexFileImage.getName());

			if (sfStart != null)
				remoteExec(conn, sfStart);

			hexFileImage.delete();
			conn.close();
		} catch (IOException e) {
			log.error(e.getMessage() + ":");
			for (StackTraceElement st : e.getStackTrace())
				log.error(st);
		}
	}

	/**
	 * Runs a command on a remote host using SSH.
	 * 
	 * @param conn
	 *            The connection to the remote host.
	 * @param cmd
	 *            The command to execute.
	 * @return The exit status of the command.
	 * @throws IOException
	 *             when an exception occurs with the connection.
	 */
	private int remoteExec(Connection conn, String cmd) throws IOException {
		log.trace("Executing '" + cmd + "' on " + conn.getHostname() + ":"
				+ conn.getPort());
		Session sess = conn.openSession();
		sess.execCommand(cmd);

		BufferedReader stdout = new BufferedReader(new InputStreamReader(
				new StreamGobbler(sess.getStdout())));
		BufferedReader stderr = new BufferedReader(new InputStreamReader(
				new StreamGobbler(sess.getStderr())));
		String line;

		while ((line = stdout.readLine()) != null)
			log.trace(line);
		while ((line = stderr.readLine()) != null)
			log.error(line);

		int exitStatus = 0;
		try {
			exitStatus = sess.getExitStatus();
		} catch (Exception e) {
			log.warn("Unable to get exit status on " + conn.getHostname() + ":"
					+ conn.getPort());
			return 0;
		}
		sess.close();
		log.trace("Returned " + exitStatus + " on " + conn.getHostname() + ":"
				+ conn.getPort());
		return exitStatus;
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see java.lang.Thread#run()
	 */
	@Override
	public void run() {
		MoteConfig moteConfig = testbedService.getConfig().getMoteConfig(id);
		if (moteConfig == null)
			return;
		String source = moteConfig.getSource();

		Type type = getSourceType(source);

		log
				.trace("Preparing node " + id + " on " + source + " of type "
						+ type);

		String cmdFlash[] = null;
		String tokens[];

		switch (type) {
		case LOCAL:
			flashLocal(source, moteConfig.getSshSFStart(), moteConfig
					.getSshSFStop());
			break;
		case NET:
			tokens = source.split(":");
			cmdFlash = new String[] { "net-flash", tokens[0], tokens[1],
					hexFileImage.getAbsoluteFile().toString() };
			execCmd(cmdFlash);
			break;
		case SSH:
			tokens = source.split("@");
			flashSSH(tokens[0], tokens[1], moteConfig.getSshSFStart(),
					moteConfig.getSshSFStop());
			break;
		}

		if (serialReceiver != null)
			serialReceiver.connectToMote(moteConfig.getSerialFw());
		task.getFlashSem().release();
	}

	/**
	 * Executes a native command.
	 * 
	 * @param cmd
	 *            The command to execute.
	 */
	private void execCmd(String[] cmd) {
		ProcessBuilder builderFlash = new ProcessBuilder(cmd);
		builderFlash.redirectErrorStream(true);
		Process processFlash;
		try {
			processFlash = builderFlash.start();
		} catch (IOException e) {
			log.error(e.getMessage());
			hexFileImage.delete();
			throw new MoteProgramException(e);
		}
		BufferedReader brFlash = new BufferedReader(new InputStreamReader(
				processFlash.getInputStream()));
		String line;
		try {
			while ((line = brFlash.readLine()) != null)
				log.trace(line);
		} catch (IOException e) {
			log.error(e.getMessage());
			hexFileImage.delete();
			throw new MoteProgramException(id,
					MoteProgramException.MoteState.FLASH, e);
		}

		try {
			if (processFlash.waitFor() != 0) {
				hexFileImage.delete();
				log.error("Unable to execute " + cmd);
				throw new MoteProgramException(id,
						MoteProgramException.MoteState.FLASH,
						"Unable to execute command");
			}
		} catch (InterruptedException e) {
			log.error(e.getMessage());
			throw new MoteProgramException(id,
					MoteProgramException.MoteState.FLASH, e);
		}

	}
}
